package 
{
	import away3d.loading.BitmapDataResource;
	import com.bit101.components.Label;
	import com.bit101.components.Slider;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.events.TimerEvent;
	import flash.filters.BlurFilter;
	import flash.geom.Matrix;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.media.Camera;
	import flash.media.Video;
	import flash.utils.ByteArray;
	import flash.utils.Timer;
	import org.flashdevelop.utils.FlashConnect;
	
	/**
	 * ...
	 * @author René Verheij
	 */

	[SWF(width="1280", height="1024", backgroundColor="#000000", frameRate="30")]
	public class Main extends Sprite 
	{
		
		private static var _instance:Main;
		public var tempPawns:Array = new Array();
		public var pawns:Array = new Array();
		public var colors:HashMap= new HashMap();
		private var timers:HashMap = new HashMap();
		private var _video:BitmapData;
		private var _source:Bitmap;
		private var _vid:Video;
		private var _processed:Bitmap;
		private var _processed2:Bitmap;
		private var _processed3:Bitmap;
		private var _processedVideo:BitmapData;
		private var _processedVideo2:BitmapData;
		private var _processedVideo3:BitmapData;
		
		public var var1:Number;
		public var var2:Number;
		public var var3:Number;
		private var detector:Detector;
		private var labels:HashMap = new HashMap();
		//number of times more missed than found that leads to removal
		private var maxMissed:Number = 15;
		private var minFound:Number = 15;
		private var removalRatio:Number = 2;
		//minimum value for found/missed to be displayed
		private var displayRatio:Number = 0.6;
		private var minMovingDistance:Number = 4;
		private var pawnId:Number = 0;
		
		private var showImage:Boolean = false;
		private var testMode:Boolean = true;
		
		public function Main():void 
		{
			_instance = this;			
			
			colors = new HashMap();
			colors.put('RED', { color:0xFF6699, delay:580, lineWidth:0.1, transition:"easeInSine", scale:300, remain:0, transitionTime:15, alpha: 0.4,endAlpha:0 } );
			colors.put('BLUE',{color:0x8888FF,delay:30,lineWidth:0.1, transition:"linear", scale:40, remain:20, transitionTime:10,alpha: 0.3,endAlpha:1});
			colors.put('GREEN',{color:0x33AA33,delay:1500,lineWidth:1, transition:"easeInSine", scale:160, remain:0, transitionTime:10,alpha: 0.05,endAlpha:0});
			colors.put('ORANGE',{color:0x668822,delay:1500,lineWidth:1, transition:"easeInSine", scale:300, remain:0, transitionTime:30,alpha: 0.05,endAlpha:0});

			
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}
		public static function get instance():Main 
		{ 
			return _instance; 
		}		
		
		private function init(e:Event = null):void 
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);
			// entry point
			var y:Number = 20;
			var x:Number = 20;
			for each(var color:Object in colors.getValues())
			{
				addButton(color,x,y);
				y += 30;
				
				var timer:Timer = new Timer(color.delay, 0);
				timer.addEventListener(TimerEvent.TIMER, pulse);
				timer.start();
			}
			detector = new Detector();
			
			
			var camW : int = 420;
			var camH : int = 320;

			// Create the camera
			var cam : Camera = Camera.getCamera();
			cam.setMode(camW, camH, stage.frameRate);
			
			// Create a video
			_vid = new Video(camW, camH);
			_vid.attachCamera(cam);
			
			// Display the camera input with the same filters (minus the blur) as the MotionTracker is using
			_video = new BitmapData(camW, camH, false, 0);
			_source = new Bitmap(_video);
			_source.x = stage.stageWidth - 450; 
			_source.y = 10;
			
			_processedVideo = new BitmapData(camW,camH, false, 0);
			_processed = new Bitmap(_processedVideo);
			_processed.x = stage.stageWidth - 450; 
			_processed.y = _source.y + _source.height + 10;
			
			/*
			_processedVideo2 = new BitmapData(camW,camH, false, 0);
			_processed2 = new Bitmap(_processedVideo2);
			_processed2.x = _source.x - camW; 
			_processed2.y = _source.y;
			
			_processedVideo3 = new BitmapData(camW,camH, false, 0);
			_processed3 = new Bitmap(_processedVideo3);
			_processed3.x = _processed2.x; 
			_processed3.y = _processed.y;
			*/
			
			//_source.filters = [new ColorMatrixFilter(_matrix.toArray())];
			
			//PICTURES
			if (showImage && !testMode)
			{
				addChild(_source);			
				addChild(_processed);			
			}
			
			//addChild(_processed2);			
			//addChild(_processed3);			
			
			// Get going!
			if (!testMode)
			{
				addEventListener(Event.ENTER_FRAME, onEnterFrameHandler);
			}
			
			
			//var slider1:Slider = new Slider("horizontal", this, _processed.x, _processed.y + _processed.height + 10, slider1Handler);
			//var slider2:Slider = new Slider("horizontal", this, _processed.x, slider1.y + slider1.height + 10, slider2Handler);
			//var slider3:Slider = new Slider("horizontal", this, _processed.x, slider2.y + slider2.height + 10, slider3Handler);
			
			//labels.put(0, new Label(this, _processed.x, slider3.y + slider3.height + 10, ''));
			//for (var i:int = 1; i < 10; i++)
			//{
			//	labels.put(i, new Label(this, _processed.x, labels.getValue(i-1).y + labels.getValue(i-1).height + 10, ''));
			//}
			
			//var slider1:Slider = new Slider("horizontal", this, _processed.x, _processed.y + _processed.height + 10, slider1Handler);
			
		}
		
		private function slider3Handler(e:Event):void 
		{
			var3 = (e.currentTarget as Slider).value;
		}
		
		private function slider2Handler(e:Event):void 
		{
			var2 = (e.currentTarget as Slider).value;
		}
		
		private function slider1Handler(e:Event):void 
		{
			var1 = (e.currentTarget as Slider).value;
		}
		
		private function onEnterFrameHandler(e:Event):void 
		{
			//FLIP IMAGE
			/*var flipVerticalMatrix:Matrix = new Matrix();
			flipVerticalMatrix.scale(-1,-1)
			flipVerticalMatrix.translate(_video.width, _video.height)
			_video.draw(_vid, flipVerticalMatrix);
			*/
			_video.draw(_vid);
			//Analyse data
			var difImage:BitmapData = detector.getDiffImage(_video);
			var blocks:Array = detector.getBlocks(difImage);
			//FlashConnect.trace('#################### '+  blocks.length + ' BLOCKS FOUND');
			
			/*if (blocks.length == 3)
			{
				FlashConnect.trace('catch!');
			}*/
			
			var pawn:Pawn;
			var block:Object;
			var dist:Number;
			var distances:Array = [];
			var i:int;
			//var notMappedBlocks:Array = [];
			
			//calculate distances between pawns and blocks
			for each(pawn in pawns)
			{
				distances[pawn.id] = [];
				
				for (i= 0; i < blocks.length; i++)
				{
					dist = Math.sqrt(Math.abs(vidToScreenX(blocks[i].rect.x) - pawn.x) ^ 2 + Math.abs(vidToScreenY(blocks[i].rect.y) - pawn.y) ^ 2);
					distances[pawn.id][i] = dist;
					//FlashConnect.trace(i + ' to p'+pawn.id+ ' = '+dist);
				}
			}
			//FlashConnect.trace(distances);
			var pawnsToCheck:Array = pawns.concat();
			var pawnMapping:HashMap = new HashMap();
			
			//map pawns to closest positioned block
			while (pawnsToCheck.length > 0)
			{
				var minDist:Number = 10000;
				var mappedBlock:Number = -1;	
				
				pawn = pawnsToCheck.pop();
				
				//FlashConnect.trace('checking p' + pawn.id + ' (' + pawnsToCheck.length + ' pawns left)');
				
				//find closest block for this pawn
				for (i = 0; i < blocks.length; i++)
				{
					//FlashConnect.trace(distances[pawn.id][i] +' < '+ minDist);
					if (distances[pawn.id][i] != null && distances[pawn.id][i] < minDist)
					{
						minDist = distances[pawn.id][i];
						mappedBlock = i;
						//FlashConnect.trace('p'+pawn.id + ' takes '+mappedBlock +'('+minDist+')');
					}
				}
				
				//if any block found to map
				if (mappedBlock != -1)
				{
					//if not already mapped
					if (!pawnMapping.containsKey(mappedBlock))
					{
						//map
						pawnMapping.put(mappedBlock,pawn);
						//FlashConnect.trace(mappedBlock + ' mapped to  p' + pawn.id + '(' + minDist + ')');
					}
					else
					{
						//(already mapped)

						var otherPawn:Pawn = pawnMapping.getValue(mappedBlock) as Pawn;
						//if this one is closer
						if (distances[otherPawn.id][mappedBlock] > minDist)
						{
							//overwrite other
							//-> old needs to be checked again
							pawnsToCheck.push(pawnMapping.getValue(mappedBlock));
							//-> but cannot use this block again (unset distance)
							distances[pawnMapping.getValue(mappedBlock).id][mappedBlock] = 10000;
							
							//map this one
							pawnMapping.removeKey(mappedBlock);
							pawnMapping.put(mappedBlock,pawn);
							
							//FlashConnect.trace('p'+pawn.id +' overwrites map of p'+otherPawn.id+' to ' + mappedBlock + ' (' + minDist + ')');
							//FlashConnect.trace('p'+otherPawn.id +' will be checked again');
						}
						else
						{
							//-> this pawn needs to be checked again, as this block cannot be used
							pawnsToCheck.push(pawn);
							//-> unset distance to block
							distances[pawn.id][mappedBlock] = 10000;
							
							//FlashConnect.trace('p'+pawn.id + ' will be checked again (someone else closer to '+mappedBlock+')');
						}
					}
				}
				else
				{
					//FlashConnect.trace('p'+pawn.id + ' missed (no blocks)');
					//MISSED MAPPING
					pawn.missedMapping();
					//notMappedBlocks.push(blocks[i]);
				}
			}
			
			//FOUND MAPPING
			for each(pawn in pawns)
			{
				if (pawnMapping.containsValue(pawn))
				{
					block = pawnMapping.getKey(pawn);
					//FlashConnect.trace('p'+pawn.id + ' found mapping:' + block);
					pawn.foundMapping();
					movePawn(pawn, blocks[block].rect);
				}
			}
			
			//UNMAPPED BLOCKS
			for (i= 0; i < blocks.length; i++)
			{
				if (!pawnMapping.containsKey(i))
				{
					var color:Object = getColorFromBlock(blocks[i]);
					pawn = new Pawn(color);
					positionPawn(pawn, blocks[i].rect);
					
					pawn.id = pawnId++;
					pawns.push(pawn);
					FlashConnect.trace(pawn.id);
					//FlashConnect.trace(i + ' creates p'+pawn.id);
				}
			}
			
			//display pawns according to new data
			for each(pawn in pawns)
			{
				//show or not?
				if ((pawn.found / pawn.missed) > displayRatio && pawn.found > minFound)
				{
					if (!contains(pawn))
					{
						//FlashConnect.trace(pawn + ' PLACED');
						addChild(pawn);
					}
					//pawn.alpha = 1;
				}
				else
				{
					if (contains(pawn))
					{
						//FlashConnect.trace(pawn + ' REMOVED');
						removeChild(pawn);
					}
					//pawn.alpha = 1 * (pawn.missedInARow / maxMissed);
				}
				
				//remove if missed too much
				if (pawn.missedInARow >= maxMissed || (pawn.missed >= maxMissed && ((pawn.missed / pawn.found) > removalRatio)))
				{
					//FlashConnect.trace(pawn + ' DELETED');
					if (contains(pawn))
					{
						removeChild(pawn);
					}
					pawns.splice(pawns.indexOf(pawn), 1);
				}
				
			}
			
			
			var blockImage:BitmapData = detector.drawBlocks(difImage,blocks);
			_processed.bitmapData = blockImage;
			
			//labels.getValue(0).text = 'fps: '+detector.fps;
			//labels.getValue(1).text = 'pawns: ' + pawns.length;
			
			//FlashConnect.trace(pawns);
			//FlashConnect.trace('#########');
		}
		
		private function vidToScreenX(x:Number):Number 
		{
			if (showImage)
			{
				return (stage.stageWidth - _video.width) * (x / _video.width);
			}
			else
			{
				return stage.stageWidth * (x / _video.width);
			}
		}
		private function vidToScreenY(y:Number):Number 
		{
			return stage.stageHeight * (y / _video.height);
		}
		
		public function positionPawn(pawn:Pawn, rect:Rectangle):void 
		{
			pawn.x = vidToScreenX(rect.x + (rect.width / 2));
			pawn.y = vidToScreenY(rect.y + (rect.height/ 2));
			//FlashConnect.trace(pawn + '  placed at '+pawn.x + ' , '+pawn.y);

		}
		private function movePawn(pawn:Pawn, rect:Rectangle):void 
		{
			var newX:Number = vidToScreenX(rect.x + (rect.width / 2));
			var newY:Number = vidToScreenY(rect.y + (rect.height / 2));
			
			var dif:Number = Math.sqrt(Math.abs(newX - pawn.x) ^ 2 + Math.abs(newY - pawn.y) ^ 2);
			
			if (dif > minMovingDistance)
			{
				//FlashConnect.trace('moving p' + pawn.id + ' ' + dif + ', '+newX+ ', '+newY+ ', '+pawn.x+ ', '+pawn.y);
				pawn.x = newX;
				pawn.y = newY;
			}
			else
			{
				//FlashConnect.trace('NOT moving p' + pawn.id + ' ' + dif + ', '+newX+ ', '+newY+ ', '+pawn.x+ ', '+pawn.y);
			}
		}
		
		
		private function getColorFromBlock(block:Object):Object
		{
			var averages:Array = Main.averageColour(_video, block.rect);
			var av:uint = averages[0] << 16 | averages[1]  << 8 | averages[2] * 0.5;
			
			FlashConnect.trace(averages + '  ' +av);
			
			var closestColor:Object;
			var minDif:int = 10000;
			var dif:int;
			for each(var color:Object in colors.getValues())
			{
				dif = Main.colorDif(color.color, av);
				if (minDif > dif)
				{
					minDif = dif;
					closestColor = color;
				}
			}
			return closestColor;

		}
		public static function toRGB (rgb: uint): Array
		{
			var r: uint = rgb>> 16 & 0xFF;
			var g: uint = rgb>> 8 & 0xFF;
			var b: uint = rgb & 0xFF;
			return [r, g, b];
		}
		public static function colorDif (col1: uint, col2: uint): uint
		{
			var colA: Array = Main.toRGB (col1);
			var colB: Array = Main.toRGB (col2);
			
			return Math.abs(colA[0] - colB[0]) + Math.abs(colA[1] - colB[1]) + Math.abs(colA[2] - colB[2]);
			
			/*
			var r: uint = Math.max (Math.max (colB [0] - (256-colA [0]), colA [0] - (256-colB [0])), 0);
			var g: uint = Math.max (Math.max (colB [1] - (256-colA [1]), colA [1] - (256-colB [1])), 0);
			var b: uint = Math.max (Math.max (colB [2] - (256-colA [2]), colA [2] - (256-colB [2])), 0);
			return r <<16 | g <<8 | b;
			*/
		}		
		
		public static function averageColour( source:BitmapData,rect:Rectangle=null ):Array
		{
			var red:Number = 0;
			var green:Number = 0;
			var blue:Number = 0;

			var count:Number = 0;
			var pixel:Number;

			if (rect == null) rect = new Rectangle(0, 0, source.width, source.height);
			
			for (var x:int = rect.x; x < rect.width+rect.x; x++)
			{
				for (var y:int = rect.y; y < rect.height+rect.y; y++)
				{
					pixel = source.getPixel(x, y);

					red += pixel >> 16 & 0xFF;
					green += pixel >> 8 & 0xFF;
					blue += pixel & 0xFF;

					count++
					
				}
			}

			red /= count;
			green /= count;
			blue /= count;

			//FlashConnect.trace('!!!'+red + '  ,' + green + '  ,' + blue + '  ,' + count );
			return [red, green, blue];
			//return red << 16 | green << 8 | blue;
		}		
		
		private function pulse(e:Event):void 
		{
			var color:Object;
			var timer:Timer = (e.currentTarget as Timer)
			for each(var tempColor:Object in colors.getValues())
			{
				if (tempColor.delay == timer.delay)
				{
					color = tempColor;
				}
			}
			for each(var pawn:Pawn in pawns)
			{
				if (pawn.color === color && contains(pawn))
				{
					pawn.pulse();
				}
			}
		}
		
		//private function addGreenPawn(e:Event):void 	{	addPawn(GREEN.color);	}
		//private function addRedPawn(e:Event):void 		{	addPawn(RED.color);	}
		//private function addBluePawn(e:Event):void 	{	addPawn(BLUE.color);	}
		
		//private function addPawn(color:Object):void 
		private function addPawn(e:Event):void 
		{
			var color:Object = colors.getValue((e.currentTarget as Sprite).name);
			var pawn:Pawn = new Pawn(color);
			pawn.x = Math.random()*(stage.stageWidth-450);
			pawn.y = Math.random()*stage.stageHeight;
			addChild(pawn);
			pawns.push(pawn);
		}
		
		private function addButton(color:Object,x:Number,y:Number,listener:Function=null):void
		{
			var button:Sprite = new Sprite();
			button.name = colors.getKey(color);
			button.graphics.lineStyle(3, 0x55cc55);
			button.graphics.beginFill(color.color);
			button.graphics.drawCircle(0, 0, 10);
			button.buttonMode = true;
			button.x = x;
			button.y = y;
			button.addEventListener(MouseEvent.CLICK, addPawn);
			
			addChild(button);
		}
		
		
	}
	
}